package fr.fonzie.javac;

import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.List;

class FonzieCodeIntroducer
    extends TreeTranslator
{

    private SunJavacContext ctx;

    private String clazz;

    FonzieCodeIntroducer( SunJavacContext context )
    {
        this.ctx = context;
    }

    @Override
    public void visitClassDef( JCClassDecl classDecl )
    {
        clazz = classDecl.getSimpleName().toString();
        super.visitClassDef( classDecl );
    }

    @Override
    public void visitMethodDef( JCMethodDecl method )
    {
        JCBlock fonzieCode;
        if ( isNative( method )
             && ( fonzieCode = introduceFonzieCode( method ) ) != null )
        {
            method.mods.flags -= Flags.NATIVE;
            method.restype = translate( method.restype );
            method.typarams = translateTypeParams( method.typarams );
            method.params = translateVarDefs( method.params );
            method.thrown = translate( method.thrown );
            method.body = fonzieCode;
        }
        else
        {
            super.visitMethodDef( method );
        }
        result = method;
    }

    /**
     * Build call to fonzie-core as replacement for the native method
     */
    private JCBlock introduceFonzieCode( JCMethodDecl method )
    {
        // TODO when return type is not void, return the entity
        // to support method chaining
        String methodName = method.getName().toString();
        if ( methodName.equals( "persist" ) )
        {
            return block( parse( "fr.fonzie.Fonzie.persist( this )" ) );
        }
        else if ( methodName.equals( "remove" ) )
        {
            return block( parse( "fr.fonzie.Fonzie.remove( this )" ) );
        }
        else if ( methodName.equals( "refresh" ) )
        {
            return block( parse( "fr.fonzie.Fonzie.refresh( this )" ) );
        }
        else if ( methodName.equals( "detach" ) )
        {
            return block( parse( "fr.fonzie.Fonzie.detach( this )" ) );
        }
        else if ( methodName.equals( "isManaged" ) )
        {
            return block( parse( "return fr.fonzie.Fonzie.isManaged( this )" ) );
        }
        else if ( methodName.equals( "getReference" ) )
        {
            return block( parse( "return fr.fonzie.Fonzie.getReference( "
                + clazz + ".class"
                + join( method.getParameters() )
                + " )" ) );
        }
        else if ( methodName.equals( "find" ) )
        {
            return block( parse( "return fr.fonzie.Fonzie.find( "
                + clazz + ".class"
                + join( method.getParameters() )
                + " )" ) );
        }
        else if ( methodName.startsWith( "findBy" ) )
        {
            String criteria = methodName.substring( 6 );
            JCTree returnType = method.getReturnType();

            String finder =
                returnType.toString().equals( clazz ) ?
                "findSingle" : "find";

            return block(
                parse( "String criteria = " + criteria ),
                parse( "return fr.fonzie.Fonzie." + finder + "( "
                    + clazz + ".class, criteria"
                    + join( method.getParameters() )
                    + " )" ) );
        }
        else
        {
            return null;
            // throw new UnsupportedOperationException( "Not supported yet" );
        }
    }

    private boolean isNative( JCMethodDecl method )
    {
        return ( method.mods.flags & Flags.NATIVE ) > 0;
    }

    private JCBlock block( JCStatement... statements )
    {
        return ctx.maker.Block( Flags.BLOCK, List.<JCStatement> from( statements ) );
    }

    private JCStatement parse( String code )
    {
        boolean doReturn = code.startsWith( "return " );
        if ( doReturn )
        {
            code = code.substring( 7 );
        }

        int k = code.indexOf( " = " );
        if ( k > 0 )
        {
            String valueExpression = code.substring( k + 3 );
            int i = code.indexOf( ' ' );
            String type = code.substring( 0, i );
            int j = code.indexOf( ' ', i + 1 );
            String varName = code.substring( i + 1, j );

            JCExpression oType = ident( type );
            JCExpression oValue = apply( valueExpression );
            // FIXME how to get oType from JCMethodInvocation ? oValue.meth.type is null :'(

            JCVariableDecl var =
                ctx.maker.VarDef( ctx.maker.Modifiers( Flags.FINAL ), ctx.fromString( varName ), oType, oValue );
            return var;
        }
        else
        {
            if ( doReturn )
            {
                return ctx.maker.Return( apply( code ) );
            }
            else
            {
                return ctx.maker.Exec( apply( code ) );
            }
        }
    }

    /**
     * Based on Lombok JavacHandlerUtil
     * <p>
     * In javac, dotted access of any kind, from {@code java.lang.String} to {@code var.methodName} is represented by a
     * fold-left of {@code Select} nodes with the leftmost string represented by a {@code Ident} node. This method
     * generates such an expression.
     * <p>
     * For example, maker.Select(maker.Select(maker.Ident(NAME[java]), NAME[lang]), NAME[String]).
     *
     * @see http://projectlombok.org/api/lombok/javac/handlers/JavacHandlerUtil.html
     */
    private JCExpression ident( String fcn )
    {
        String[] parts = fcn.split( "\\." );
        JCExpression e = ctx.maker.Ident( ctx.fromString( parts[0] ) );
        for ( int i = 1; i < parts.length; i++ )
        {
            e = ctx.maker.Select( e, ctx.fromString( parts[i] ) );
        }
        return e;
    }

    private JCExpression apply( String expresssion )
    {
        if ( expresssion.endsWith( ")" ) )
        {
            return methodCall( expresssion );
        }
        else
        {
            return ctx.maker.Literal( expresssion );
        }
    }

    private JCMethodInvocation methodCall( String methodCall )
    {
        int i = methodCall.indexOf( '(' );
        String fcn = methodCall.substring( 0, i );
        String argString = methodCall.substring( i + 1, methodCall.length() - 1 );
        List<JCExpression> params = nil;
        if ( argString.length() > 0 )
        {
            String[] args = argString.split( "," );
            JCExpression[] expressions = new JCExpression[args.length];
            int idx = 0;
            for ( String arg : args )
            {
                expressions[idx++] = ident( arg.trim() );
            }
            params = List.<JCExpression> from( expressions );
        }

        return apply( fcn, params );
    }

    private final List<JCExpression> nil = List.<JCExpression> nil();

    private JCMethodInvocation apply( String fcn, List<JCExpression> args )
    {
        JCExpression oMethod = ident( fcn );
        JCMethodInvocation oValue = ctx.maker.Apply( nil, oMethod, args );
        return oValue;
    }

    private String join( List<JCVariableDecl> parameters )
    {
        StringBuilder s = new StringBuilder();
        for ( JCVariableDecl var : parameters )
        {
            s.append( ", " ).append( var.getName().toString() );
        }
        return s.toString();
    }
}